Coverage Report

Created: 2026-08-07 16:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\cssh-rs\cssh-rs\xtask\src\coverage.rs
Line
Count
Source
1
//! Local coverage report generation.
2
//!
3
//! The nightly toolchain is required because `#[coverage(off)]` - used to
4
//! exclude untestable code such as Windows API wrappers and production I/O
5
//! implementations from coverage - relies on the `coverage_attribute` feature,
6
//! which is only available on nightly Rust. Without it the `cfg(coverage_nightly)`
7
//! guards would not activate, causing those impls to be counted as missed lines
8
//! and distorting the report.
9
//!
10
//! The pinned toolchain version is read from
11
//! `.config/coverage/nightly-toolchain.version` and the filename exclusion
12
//! regex is read from `.config/coverage/ignore-filename.regex`; both files
13
//! are shared with the CI workflow to keep the environments in sync.
14
//!
15
//! [`run_coverage`] orchestrates the full workflow: toolchain check,
16
//! instrumented test run, and report generation.
17
18
use anyhow::{bail, Context, Result};
19
20
/// All side-effecting operations required by this module.
21
///
22
/// Implement with mocks in tests to achieve zero filesystem, process,
23
/// and toolchain side-effects.
24
pub trait CoverageSystem {
25
    /// Read the contents of `.config/coverage/nightly-toolchain.version` and
26
    /// return the trimmed toolchain identifier (e.g. `nightly-2026-04-20`).
27
    ///
28
    /// # Errors
29
    ///
30
    /// Returns an error if the file cannot be read.
31
    fn read_nightly_version_file(&self) -> Result<String>;
32
33
    /// Run `rustup toolchain list` and return its stdout.
34
    ///
35
    /// # Errors
36
    ///
37
    /// Returns an error if the process cannot be started.
38
    fn list_installed_toolchains(&self) -> Result<String>;
39
40
    /// Run `rustup toolchain install <toolchain> --component llvm-tools`.
41
    ///
42
    /// # Errors
43
    ///
44
    /// Returns an error if the install fails.
45
    fn install_toolchain(&self, toolchain: &str) -> Result<()>;
46
47
    /// Run a `cargo +<toolchain> llvm-cov` subcommand with the given arguments.
48
    ///
49
    /// # Errors
50
    ///
51
    /// Returns an error if the command fails.
52
    fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()>;
53
54
    /// Read the contents of `.config/coverage/ignore-filename.regex` and
55
    /// return the trimmed filename regex pattern passed to
56
    /// `--ignore-filename-regex`.
57
    ///
58
    /// # Errors
59
    ///
60
    /// Returns an error if the file cannot be read.
61
    fn read_ignore_regex_file(&self) -> Result<String>;
62
}
63
64
/// Production implementation of [`CoverageSystem`].
65
pub struct RealSystem;
66
67
#[cfg_attr(coverage_nightly, coverage(off))]
68
impl CoverageSystem for RealSystem {
69
    fn read_nightly_version_file(&self) -> Result<String> {
70
        std::fs::read_to_string(".config/coverage/nightly-toolchain.version")
71
            .context("failed to read .config/coverage/nightly-toolchain.version")
72
            .map(|s| s.trim().to_owned())
73
    }
74
75
    fn list_installed_toolchains(&self) -> Result<String> {
76
        let output = std::process::Command::new("rustup")
77
            .args(["toolchain", "list"])
78
            .output()
79
            .context("failed to run `rustup toolchain list`")?;
80
        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
81
    }
82
83
    fn install_toolchain(&self, toolchain: &str) -> Result<()> {
84
        let status = std::process::Command::new("rustup")
85
            .args([
86
                "toolchain",
87
                "install",
88
                toolchain,
89
                "--component",
90
                "llvm-tools",
91
            ])
92
            .status()
93
            .context("failed to run `rustup toolchain install`")?;
94
        if !status.success() {
95
            bail!("`rustup toolchain install {toolchain}` failed with status {status}");
96
        }
97
        Ok(())
98
    }
99
100
    fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()> {
101
        let toolchain_arg = format!("+{toolchain}");
102
        let status = std::process::Command::new("cargo")
103
            .arg(&toolchain_arg)
104
            .arg("llvm-cov")
105
            .args(args)
106
            .status()
107
            .with_context(|| {
108
                format!(
109
                    "failed to run `cargo {toolchain_arg} llvm-cov {}`",
110
                    args.join(" ")
111
                )
112
            })?;
113
        if !status.success() {
114
            bail!(
115
                "`cargo {toolchain_arg} llvm-cov {}` failed with status {status}",
116
                args.join(" ")
117
            );
118
        }
119
        Ok(())
120
    }
121
122
    fn read_ignore_regex_file(&self) -> Result<String> {
123
        std::fs::read_to_string(".config/coverage/ignore-filename.regex")
124
            .context("failed to read .config/coverage/ignore-filename.regex")
125
            .map(|s| s.trim().to_owned())
126
    }
127
}
128
129
/// Convert a slice of string literals to a `Vec<String>`.
130
13
fn args(values: &[&str]) -> Vec<String> {
131
59
    
values13
.
iter13
().
map13
(|s| (*s).to_owned()).
collect13
()
132
13
}
133
134
/// Generate coverage reports using the pinned nightly toolchain.
135
///
136
/// Reads the toolchain identifier from
137
/// `.config/coverage/nightly-toolchain.version`, ensures it is installed,
138
/// cleans stale coverage data, runs the test suite with instrumentation,
139
/// and produces Cobertura XML and HTML reports.
140
///
141
/// # Arguments
142
///
143
/// * `system` - Injected I/O provider.
144
///
145
/// # Errors
146
///
147
/// Returns an error if any step fails (missing version file, toolchain
148
/// install failure, test failure, or report generation failure).
149
6
pub fn run_coverage<S: CoverageSystem>(system: &S) -> Result<()> {
150
6
    let 
toolchain5
= system.read_nightly_version_file()
?1
;
151
5
    log::info!("Using nightly toolchain: {toolchain}");
152
5
    let ignore_regex = system.read_ignore_regex_file()
?0
;
153
154
    // Ensure toolchain is installed.
155
5
    let installed = system.list_installed_toolchains()
?0
;
156
5
    if installed.lines().any(|line| line.starts_with(&toolchain)) {
157
3
        log::info!("Toolchain already installed");
158
    } else {
159
2
        log::info!("Installing toolchain: {toolchain}");
160
2
        system.install_toolchain(&toolchain)
?1
;
161
    }
162
163
    // Clean previous coverage data.
164
4
    log::info!("Cleaning previous coverage data");
165
4
    system.run_cargo_llvm_cov(&toolchain, &args(&["clean", "--workspace"]))
?1
;
166
167
    // Run tests with coverage instrumentation.
168
3
    log::info!("Running tests with coverage");
169
3
    system.run_cargo_llvm_cov(
170
3
        &toolchain,
171
3
        &args(&[
172
3
            "--all-features",
173
3
            "--workspace",
174
3
            "--no-report",
175
3
            "--",
176
3
            "--no-capture",
177
3
        ]),
178
0
    )?;
179
180
    // Generate Cobertura XML report.
181
3
    log::info!("Generating Cobertura XML report");
182
3
    system.run_cargo_llvm_cov(
183
3
        &toolchain,
184
3
        &args(&[
185
3
            "report",
186
3
            "--cobertura",
187
3
            "--output-path",
188
3
            "coverage.xml",
189
3
            "--ignore-filename-regex",
190
3
            &ignore_regex,
191
3
        ]),
192
0
    )?;
193
194
    // Generate HTML report.
195
3
    log::info!("Generating HTML report");
196
3
    system.run_cargo_llvm_cov(
197
3
        &toolchain,
198
3
        &args(&[
199
3
            "report",
200
3
            "--html",
201
3
            "--output-dir",
202
3
            "coverage_html",
203
3
            "--ignore-filename-regex",
204
3
            &ignore_regex,
205
3
        ]),
206
0
    )?;
207
208
3
    log::info!("Coverage reports generated:");
209
3
    log::info!("  XML:  coverage.xml");
210
3
    log::info!("  HTML: coverage_html/index.html");
211
3
    Ok(())
212
6
}
213
214
#[cfg(test)]
215
#[path = "tests/test_coverage.rs"]
216
mod tests;